use std::future::IntoFuture;

use anyhow::{Result, bail};
use next_core::{
    middleware::get_middleware_module,
    next_edge::entry::wrap_edge_entry,
    next_manifests::{EdgeFunctionDefinition, MiddlewaresManifestV2, ProxyMatcher, Regions},
    segment_config::NextSegmentConfig,
    util::{MiddlewareMatcherKind, NextRuntime},
};
use tracing::Instrument;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{Completion, ResolvedVc, Vc};
use turbo_tasks_fs::{self, File, FileContent, FileSystemPath};
use turbopack_core::{
    asset::AssetContent,
    chunk::{
        ChunkingContext, ChunkingContextExt, EntryChunkGroupResult,
        availability_info::AvailabilityInfo,
    },
    context::AssetContext,
    module::Module,
    module_graph::{
        GraphEntries,
        chunk_group_info::{ChunkGroup, ChunkGroupEntry},
    },
    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
    reference_type::{EntryReferenceSubType, ReferenceType},
    source::Source,
    virtual_output::VirtualOutputAsset,
};

use crate::{
    nft_json::NftJsonAsset,
    paths::{
        all_paths_in_root, all_server_paths, get_asset_paths_from_root, get_js_paths_from_root,
        get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings,
    },
    project::Project,
    route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs},
};

#[turbo_tasks::value]
pub struct MiddlewareEndpoint {
    project: ResolvedVc<Project>,
    asset_context: ResolvedVc<Box<dyn AssetContext>>,
    source: ResolvedVc<Box<dyn Source>>,
    app_dir: Option<FileSystemPath>,
    ecmascript_client_reference_transition_name: Option<RcStr>,
    config: ResolvedVc<NextSegmentConfig>,
    runtime: NextRuntime,
}

#[turbo_tasks::value_impl]
impl MiddlewareEndpoint {
    #[turbo_tasks::function]
    pub fn new(
        project: ResolvedVc<Project>,
        asset_context: ResolvedVc<Box<dyn AssetContext>>,
        source: ResolvedVc<Box<dyn Source>>,
        app_dir: Option<FileSystemPath>,
        ecmascript_client_reference_transition_name: Option<RcStr>,
        config: ResolvedVc<NextSegmentConfig>,
        runtime: NextRuntime,
    ) -> Vc<Self> {
        Self {
            project,
            asset_context,
            source,
            app_dir,
            ecmascript_client_reference_transition_name,
            config,
            runtime,
        }
        .cell()
    }

    #[turbo_tasks::function]
    async fn entry_module(&self) -> Result<Vc<Box<dyn Module>>> {
        let userland_module = self
            .asset_context
            .process(
                *self.source,
                ReferenceType::Entry(EntryReferenceSubType::Middleware),
            )
            .module();

        let userland_path = userland_module.ident().path().await?;
        let is_proxy = userland_path.file_stem() == Some("proxy");

        let module = get_middleware_module(
            *self.asset_context,
            self.project.project_path().owned().await?,
            userland_module,
            is_proxy,
        );

        if matches!(self.runtime, NextRuntime::NodeJs) {
            return Ok(module);
        }

        Ok(wrap_edge_entry(
            *self.asset_context,
            self.project.project_path().owned().await?,
            module,
            rcstr!("middleware"),
        ))
    }

    #[turbo_tasks::function]
    async fn edge_chunk_group(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
        let this = self.await?;
        let module = self.entry_module().to_resolved().await?;

        let module_graph = this.project.module_graph(*module);

        let edge_chunking_context = this.project.edge_chunking_context(false);
        let edge_chunk_grou = edge_chunking_context.evaluated_chunk_group_assets(
            module.ident(),
            ChunkGroup::Entry(vec![module]),
            module_graph,
            AvailabilityInfo::root(),
        );
        Ok(edge_chunk_grou)
    }

    #[turbo_tasks::function]
    async fn node_chunk(self: Vc<Self>) -> Result<Vc<Box<dyn OutputAsset>>> {
        let this = self.await?;

        let chunking_context = this.project.server_chunking_context(false);

        let userland_module = self.entry_module().to_resolved().await?;
        let module_graph = this.project.module_graph(*userland_module);

        let Some(module) = ResolvedVc::try_downcast(userland_module) else {
            bail!("Entry module must be evaluatable");
        };

        let EntryChunkGroupResult { asset: chunk, .. } = *chunking_context
            .entry_chunk_group(
                this.project
                    .node_root()
                    .await?
                    .join("server/middleware.js")?,
                Vc::cell(vec![module]),
                module_graph,
                OutputAssets::empty(),
                OutputAssets::empty(),
                AvailabilityInfo::root(),
            )
            .await?;
        Ok(*chunk)
    }

    #[turbo_tasks::function]
    async fn output_assets(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
        let this = self.await?;
        let config = this.config.await?;

        let next_config = this.project.next_config();
        let i18n = next_config.i18n().await?;
        let has_i18n = i18n.is_some();
        let has_i18n_locales = i18n
            .as_ref()
            .map(|i18n| i18n.locales.len() > 1)
            .unwrap_or(false);
        let base_path = next_config.base_path().await?;

        let matchers = if let Some(matchers) = config.middleware_matcher.as_ref() {
            matchers
                .iter()
                .map(|matcher| {
                    let mut matcher = match matcher {
                        MiddlewareMatcherKind::Str(matcher) => ProxyMatcher {
                            original_source: matcher.as_str().into(),
                            ..Default::default()
                        },
                        MiddlewareMatcherKind::Matcher(matcher) => matcher.clone(),
                    };

                    // Mirrors implementation in get-page-static-info.ts getMiddlewareMatchers
                    let mut source = matcher.original_source.to_string();
                    let is_root = source == "/";
                    let has_locale = matcher.locale;

                    if has_i18n_locales && has_locale {
                        if is_root {
                            source.clear();
                        }
                        source.insert_str(0, "/:nextInternalLocale((?!_next/)[^/.]{1,})");
                    }

                    if is_root {
                        source.push('(');
                        if has_i18n {
                            source.push_str("|\\\\.json|");
                        }
                        source.push_str("/?index|/?index\\\\.json)?")
                    } else {
                        source.push_str("{(\\\\.json)}?")
                    };

                    source.insert_str(0, "/:nextData(_next/data/[^/]{1,})?");

                    if let Some(base_path) = base_path.as_ref() {
                        source.insert_str(0, base_path);
                    }

                    // TODO: The implementation of getMiddlewareMatchers outputs a regex here
                    // using path-to-regexp. Currently there is no
                    // equivalent of that so it post-processes
                    // this value to the relevant regex in manifest-loader.ts
                    matcher.regexp = Some(RcStr::from(source));

                    matcher
                })
                .collect()
        } else {
            vec![ProxyMatcher {
                regexp: Some(rcstr!("^/.*$")),
                original_source: rcstr!("/:path*"),
                ..Default::default()
            }]
        };

        if matches!(this.runtime, NextRuntime::NodeJs) {
            let chunk = self.node_chunk().to_resolved().await?;
            let mut output_assets = vec![chunk];
            if this.project.next_mode().await?.is_production() {
                output_assets.push(ResolvedVc::upcast(
                    NftJsonAsset::new(*this.project, None, *chunk, vec![])
                        .to_resolved()
                        .await?,
                ));
            }
            let middleware_manifest_v2 = MiddlewaresManifestV2 {
                middleware: [].into_iter().collect(),
                ..Default::default()
            };
            let middleware_manifest_v2 = VirtualOutputAsset::new(
                this.project
                    .node_root()
                    .await?
                    .join("server/middleware/middleware-manifest.json")?,
                AssetContent::file(
                    FileContent::Content(File::from(serde_json::to_string_pretty(
                        &middleware_manifest_v2,
                    )?))
                    .cell(),
                ),
            )
            .to_resolved()
            .await?;
            output_assets.push(ResolvedVc::upcast(middleware_manifest_v2));

            Ok(Vc::cell(output_assets))
        } else {
            let edge_chunk_group = self.edge_chunk_group();
            let edge_all_assets = edge_chunk_group.expand_all_assets();

            let node_root = this.project.node_root().owned().await?;
            let node_root_value = node_root.clone();

            let file_paths_from_root =
                get_js_paths_from_root(&node_root_value, &edge_chunk_group.await?.assets.await?)
                    .await?;

            let mut output_assets = edge_chunk_group.all_assets().owned().await?;

            let wasm_paths_from_root =
                get_wasm_paths_from_root(&node_root_value, edge_all_assets.await?).await?;

            let all_assets =
                get_asset_paths_from_root(&node_root_value, &edge_all_assets.await?).await?;

            let regions = if let Some(regions) = config.preferred_region.as_ref() {
                if regions.len() == 1 {
                    regions
                        .first()
                        .map(|region| Regions::Single(region.clone()))
                } else {
                    Some(Regions::Multiple(regions.clone()))
                }
            } else {
                None
            };

            let edge_function_definition = EdgeFunctionDefinition {
                files: file_paths_from_root,
                wasm: wasm_paths_to_bindings(wasm_paths_from_root).await?,
                assets: paths_to_bindings(all_assets),
                name: rcstr!("middleware"),
                page: rcstr!("/"),
                regions,
                matchers: matchers.clone(),
                env: this.project.edge_env().owned().await?,
            };
            let middleware_manifest_v2 = MiddlewaresManifestV2 {
                middleware: [(rcstr!("/"), edge_function_definition)]
                    .into_iter()
                    .collect(),
                ..Default::default()
            };
            let middleware_manifest_v2 = VirtualOutputAsset::new(
                node_root.join("server/middleware/middleware-manifest.json")?,
                AssetContent::file(
                    FileContent::Content(File::from(serde_json::to_string_pretty(
                        &middleware_manifest_v2,
                    )?))
                    .cell(),
                ),
            )
            .to_resolved()
            .await?;
            output_assets.push(ResolvedVc::upcast(middleware_manifest_v2));

            Ok(Vc::cell(output_assets))
        }
    }

    #[turbo_tasks::function]
    fn userland_module(&self) -> Vc<Box<dyn Module>> {
        self.asset_context
            .process(
                *self.source,
                ReferenceType::Entry(EntryReferenceSubType::Middleware),
            )
            .module()
    }
}

#[turbo_tasks::value_impl]
impl Endpoint for MiddlewareEndpoint {
    #[turbo_tasks::function]
    async fn output(self: ResolvedVc<Self>) -> Result<Vc<EndpointOutput>> {
        let span = tracing::info_span!("middleware endpoint");
        async move {
            let this = self.await?;
            let output_assets = self.output_assets();

            let (server_paths, client_paths) = if this.project.next_mode().await?.is_development() {
                let node_root = this.project.node_root().owned().await?;
                let server_paths = all_server_paths(output_assets, node_root).owned().await?;

                // Middleware could in theory have a client path (e.g. `new URL`).
                let client_relative_root = this.project.client_relative_path().owned().await?;
                let client_paths = all_paths_in_root(output_assets, client_relative_root)
                    .into_future()
                    .owned()
                    .instrument(tracing::info_span!("client_paths"))
                    .await?;
                (server_paths, client_paths)
            } else {
                (vec![], vec![])
            };

            Ok(EndpointOutput {
                output_paths: EndpointOutputPaths::Edge {
                    server_paths,
                    client_paths,
                }
                .resolved_cell(),
                output_assets: output_assets.to_resolved().await?,
                project: this.project,
            }
            .cell())
        }
        .instrument(span)
        .await
    }

    #[turbo_tasks::function]
    async fn server_changed(self: Vc<Self>) -> Result<Vc<Completion>> {
        Ok(self.await?.project.server_changed(self.output_assets()))
    }

    #[turbo_tasks::function]
    fn client_changed(self: Vc<Self>) -> Vc<Completion> {
        Completion::immutable()
    }

    #[turbo_tasks::function]
    async fn entries(self: Vc<Self>) -> Result<Vc<GraphEntries>> {
        Ok(Vc::cell(vec![ChunkGroupEntry::Entry(vec![
            self.entry_module().to_resolved().await?,
        ])]))
    }

    #[turbo_tasks::function]
    async fn module_graphs(self: Vc<Self>) -> Result<Vc<ModuleGraphs>> {
        let this = self.await?;
        let module_graph = this
            .project
            .module_graph(self.entry_module())
            .to_resolved()
            .await?;
        Ok(Vc::cell(vec![module_graph]))
    }
}
